//@author A0087191Y
package net.mysoc.w111j.model;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;

/**
 * Immutable representation of a amount of money. Performing arithmetic operations will return a new {@code Money}
 * object.
 *
 * In terms of implementation details, {@code Money} objects are simple wrappers around {@link java.math.BigDecimal}
 * objects.
 */
public final class Money implements Comparable<Money>, Serializable {
    private static final int DEFAULT_SCALE = 2;
    private static final RoundingMode DEFAULT_ROUNDING = RoundingMode.HALF_EVEN;
    /** The amount of money being represented by this object. */
    private BigDecimal amount;

    /**
     * Default constructor. Creates a {@code Money} object with amount initialised to zero.
     */
    public Money() {
        setAmount(BigDecimal.ZERO);
    }

    /**
     * Creates a {@code Money} object amount initialised from a {@code Number}.
     *
     * @param amount the monetary value being represented. Must be non-null.
     */
    public Money(Number amount) {
        this(amount.toString());
    }

    /**
     * Creates a {@code Money} object amount initialised from a {@code BigDecimal}.
     *
     * @param amount the monetary value being represented. Must be non-null.
     */
    public Money(BigDecimal amount) {
        if (amount == null) {
            throw new NullPointerException("Amount cannot be null");
        }
        setAmount(amount);
    }

    /**
     * Creates a {@code Money} object amount initialised from a {@code String}.
     *
     * @param amount the monetary value being represented. Must be non-null.
     */
    public Money(String amount) {
        if (amount == null) {
            throw new NullPointerException("Amount cannot be null");
        }
        setAmount(new BigDecimal(amount));
    }

    private void setAmount(BigDecimal amount) {
        this.amount = amount.setScale(DEFAULT_SCALE, DEFAULT_ROUNDING);
    }

    /**
     * Converts this object to a {@code BigDecimal}.
     *
     * @return the same value as a {@code BigDecimal}.
     */
    public BigDecimal toBigDecimal() {
        return amount;
    }

    /**
     * Returns a BigDecimal whose amount is {@code (this + augend)}.
     *
     * @param augend amount to be added.
     * @return {@code this + augend}
     */
    public Money add(Money augend) {
        return new Money(amount.add(augend.amount));
    }

    /**
     * Returns a BigDecimal whose amount is {@code (this - subtrahend)}.
     *
     * @param subtrahend amount to be subtracted.
     * @return {@code this - subtrahend}
     */
    public Money subtract(Money subtrahend) {
        return new Money(amount.subtract(subtrahend.amount));
    }

    /**
     * Returns a BigDecimal whose amount is {@code (this * multiplicand)}.
     *
     * @param multiplicand amount to be multiplied by.
     * @return {@code this * multiplicand}
     */
    public Money multiply(Money multiplicand) {
        return new Money(amount.multiply(multiplicand.amount));
    }

    /**
     * Returns a BigDecimal whose amount is {@code (this * multiplicand)}.
     *
     * @param multiplicand amount to be multiplied by.
     * @return {@code this * multiplicand}
     */
    public Money multiply(int multiplicand) {
        return new Money(amount.multiply(new BigDecimal(multiplicand)));
    }

    /**
     * Returns a BigDecimal whose amount is {@code (this / divisor)}.
     *
     * @param divisor amount to be divided by.
     * @return {@code this / divisor}
     */
    public Money divide(Money divisor) {
        return new Money(amount.divide(divisor.amount, amount.precision(),
                RoundingMode.HALF_EVEN));
    }

    /**
     * Returns a BigDecimal whose amount is {@code (this / divisor)}.
     *
     * @param divisor amount to be divided by.
     * @return {@code this / divisor}
     */
    public Money divide(int divisor) {
        return new Money(amount.divide(new BigDecimal(divisor),
                amount.precision(), RoundingMode.HALF_EVEN));
    }

    /**
     * Returns a BigDecimal whose amount is {@code (-this)}.
     *
     * @return {@code -this}
     */
    public Money negate() {
        return new Money(amount.negate());
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (obj == this)
            return true;
        if (obj.getClass() != getClass())
            return false;

        Money that = (Money) obj;
        // cannot use amount.equals() as that checks scale as well
        return amount.compareTo(that.amount) == 0;
    }

    @Override
    public int compareTo(Money that) {
        return amount.compareTo(that.amount);
    }

    /**
     * Obtains a {@code String} representation of this object, applying monetary formatting.
     *
     * @return {@code String} of this object.
     */
    @Override
    public String toString() {
        DecimalFormat formatter = new DecimalFormat("###,##0.00");
        formatter.setGroupingSize(3);

        return formatter.format(amount.doubleValue());
    }

    /**
     * Obtains a {@code String} representation of this object without any monetary formatting.
     *
     * @return {@code String} of this object.
     */
    public String toPlainString() {
        return amount.toPlainString();
    }
}
